﻿// ===============================================================================
//  Copyright © ymind.net. All rights reserved .
//  官方网站：http://ymind.net/
//  版权所有：彦铭工作室 老陈出品
// ===============================================================================

using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Xml;
using Lcsoft.RssEngine.Collections;
using Lcsoft.RssEngine.RssChannel;
using Lcsoft.RssEngine.RssItem;
using Lcsoft.RssEngine.Shared;

namespace Lcsoft.RssEngine
{
	/// <summary>Reads an RSS file.</summary>
	/// <remarks>Provides fast, non-cached, forward-only access to RSS data.</remarks>
	public class RssReader
	{
		// TODO: Add support for modules

		private RssCategory _category;

		private RssChannel.RssChannel _channel;
		private RssCloud _cloud;
		private StringBuilder _elementText = new StringBuilder();
		private RssEnclosure _enclosure;
		private RssGuid _guid;
		private RssImage _image;
		private RssItem.RssItem _item;
		private XmlTextReader _reader;
		private RssSource _source;
		private RssTextInput _textInput;
		private bool _wroteChannel;
		private Stack _xmlNodeStack = new Stack();

		/// <summary>A collection of all exceptions the RssReader class has encountered.</summary>
		public ExceptionCollection Exceptions { get; private set; }

		/// <summary>Gets the RSS version of the stream.</summary>
		/// <value>One of the <see cref="RssVersion"/> values.</value>
		public RssVersion Version { get; private set; }

		private void _InitReader()
		{
			this._reader.WhitespaceHandling = WhitespaceHandling.None;
			this._reader.XmlResolver = null;
		}

		/// <summary>Reads the next RssElement from the stream.</summary>
		/// <returns>An RSS Element</returns>
		/// <exception cref="InvalidOperationException">RssReader has been closed, and can not be read.</exception>
		/// <exception cref="System.IO.FileNotFoundException">RSS file not found.</exception>
		/// <exception cref="System.Xml.XmlException">Invalid XML syntax in RSS file.</exception>
		/// <exception cref="System.IO.EndOfStreamException">Unable to read an RssElement. Reached the end of the stream.</exception>
		public IRssElement Read()
		{
			var readData = false;
			IRssElement rssElement = null;
			var lineNumber = -1;
			var linePosition = -1;

			if (this._reader == null) throw new InvalidOperationException("RssReader has been closed, and can not be read.");

			do
			{
				var pushElement = true;
				try
				{
					readData = this._reader.Read();
				}
				catch (EndOfStreamException e)
				{
					throw new EndOfStreamException("Unable to read an RssElement. Reached the end of the stream.", e);
				}
				catch (XmlException e)
				{
					if (lineNumber != -1 || linePosition != -1) if (this._reader.LineNumber == lineNumber && this._reader.LinePosition == linePosition) throw this.Exceptions.LastException;

					lineNumber = this._reader.LineNumber;
					linePosition = this._reader.LinePosition;

					this.Exceptions.Add(e); // just add to list of exceptions and continue :)
				}
				if (!readData) continue;
				var readerName = this._reader.Name.ToLower();
				switch (this._reader.NodeType)
				{
					case XmlNodeType.Element:
					{
						if (this._reader.IsEmptyElement) break;
						this._elementText = new StringBuilder();

						switch (readerName)
						{
							case "item":
								// is this the end of the channel element? (absence of </channel> before <item>)
								if (!this._wroteChannel)
								{
									this._wroteChannel = true;
									rssElement = this._channel; // return RssChannel
									readData = false;
								}
								this._item = new RssItem.RssItem(); // create new RssItem
								this._channel.Items.Add(this._item);
								break;
							case "source":
								this._source = new RssSource();
								this._item.Source = this._source;
								for (var i = 0; i < this._reader.AttributeCount; i++)
								{
									this._reader.MoveToAttribute(i);
									switch (this._reader.Name.ToLower())
									{
										case "url":
											try
											{
												this._source.Url = new Uri(this._reader.Value);
											}
											catch (Exception e)
											{
												this.Exceptions.Add(e);
											}
											break;
									}
								}
								break;
							case "enclosure":
								this._enclosure = new RssEnclosure();
								this._item.Enclosure = this._enclosure;
								for (var i = 0; i < this._reader.AttributeCount; i++)
								{
									this._reader.MoveToAttribute(i);
									switch (this._reader.Name.ToLower())
									{
										case "url":
											try
											{
												this._enclosure.Url = new Uri(this._reader.Value);
											}
											catch (Exception e)
											{
												this.Exceptions.Add(e);
											}
											break;
										case "length":
											try
											{
												this._enclosure.Length = int.Parse(this._reader.Value);
											}
											catch (Exception e)
											{
												this.Exceptions.Add(e);
											}
											break;
										case "type":
											this._enclosure.Type = this._reader.Value;
											break;
									}
								}
								break;
							case "guid":
								this._guid = new RssGuid();
								this._item.Guid = this._guid;
								for (var i = 0; i < this._reader.AttributeCount; i++)
								{
									this._reader.MoveToAttribute(i);
									switch (this._reader.Name.ToLower())
									{
										case "ispermalink":
											try
											{
												this._guid.PermaLink = bool.Parse(this._reader.Value);
											}
											catch (Exception e)
											{
												this.Exceptions.Add(e);
											}
											break;
									}
								}
								break;
							case "category":
								this._category = new RssCategory();
								if ((string)this._xmlNodeStack.Peek() == "channel") this._channel.Categories.Add(this._category);
								else this._item.Categories.Add(this._category);
								for (var i = 0; i < this._reader.AttributeCount; i++)
								{
									this._reader.MoveToAttribute(i);
									switch (this._reader.Name.ToLower())
									{
										case "url":
											goto case "domain";
										case "domain":
											this._category.Domain = this._reader.Value;
											break;
									}
								}
								break;
							case "channel":
								this._channel = new RssChannel.RssChannel();
								this._textInput = null;
								this._image = null;
								this._cloud = null;
								this._source = null;
								this._enclosure = null;
								this._category = null;
								this._item = null;
								break;
							case "image":
								this._image = new RssImage();
								this._channel.Image = this._image;
								break;
							case "textinput":
								this._textInput = new RssTextInput();
								this._channel.TextInput = this._textInput;
								break;
							case "cloud":
								pushElement = false;
								this._cloud = new RssCloud();
								this._channel.Cloud = this._cloud;
								for (var i = 0; i < this._reader.AttributeCount; i++)
								{
									this._reader.MoveToAttribute(i);
									switch (this._reader.Name.ToLower())
									{
										case "domain":
											this._cloud.Domain = this._reader.Value;
											break;
										case "port":
											try
											{
												this._cloud.Port = ushort.Parse(this._reader.Value);
											}
											catch (Exception e)
											{
												this.Exceptions.Add(e);
											}
											break;
										case "path":
											this._cloud.Path = this._reader.Value;
											break;
										case "registerprocedure":
											this._cloud.RegisterProcedure = this._reader.Value;
											break;
										case "protocol":
											switch (this._reader.Value.ToLower())
											{
												case "xml-rpc":
													this._cloud.Protocol = RssCloudProtocol.XmlRpc;
													break;
												case "soap":
													this._cloud.Protocol = RssCloudProtocol.Soap;
													break;
												case "http-post":
													this._cloud.Protocol = RssCloudProtocol.HttpPost;
													break;
												default:
													this._cloud.Protocol = RssCloudProtocol.Empty;
													break;
											}
											break;
									}
								}
								break;
							case "rss":
								for (var i = 0; i < this._reader.AttributeCount; i++)
								{
									this._reader.MoveToAttribute(i);
									if (this._reader.Name.ToLower() != "version") continue;
									switch (this._reader.Value)
									{
										case "0.91":
											this.Version = RssVersion.Rss091;
											break;
										case "0.92":
											this.Version = RssVersion.Rss092;
											break;
										case "2.0":
											this.Version = RssVersion.Rss20;
											break;
										default:
											this.Version = RssVersion.NotSupported;
											break;
									}
								}
								break;
							case "rdf":
								for (var i = 0; i < this._reader.AttributeCount; i++)
								{
									this._reader.MoveToAttribute(i);

									if (this._reader.Name.ToLower() != "version") continue;
									switch (this._reader.Value)
									{
										case "0.90":
											this.Version = RssVersion.Rss090;
											break;
										case "1.0":
											this.Version = RssVersion.Rss10;
											break;
										default:
											this.Version = RssVersion.NotSupported;
											break;
									}
								}
								break;
						}
						if (pushElement) this._xmlNodeStack.Push(readerName);
						break;
					}
					case XmlNodeType.EndElement:
					{
						if (this._xmlNodeStack.Count == 1) break;
						var childElementName = (string)this._xmlNodeStack.Pop();
						var parentElementName = (string)this._xmlNodeStack.Peek();
						switch (childElementName) // current element
						{
								// item classes
							case "item":
								rssElement = this._item;
								readData = false;
								break;
							case "source":
								this._source.Name = this._elementText.ToString();
								rssElement = this._source;
								readData = false;
								break;
							case "enclosure":
								rssElement = this._enclosure;
								readData = false;
								break;
							case "guid":
								this._guid.Name = this._elementText.ToString();
								rssElement = this._guid;
								readData = false;
								break;
							case "category": // parent is either item or channel
								this._category.Name = this._elementText.ToString();
								rssElement = this._category;
								readData = false;
								break;
								// channel classes
							case "channel":
								if (this._wroteChannel) this._wroteChannel = false;
								else
								{
									this._wroteChannel = true;
									rssElement = this._channel;
									readData = false;
								}
								break;
							case "textinput":
								rssElement = this._textInput;
								readData = false;
								break;
							case "image":
								rssElement = this._image;
								readData = false;
								break;
							case "cloud":
								rssElement = this._cloud;
								readData = false;
								break;
						}
						switch (parentElementName) // parent element
						{
							case "item":
								switch (childElementName)
								{
									case "title":
										this._item.Title = this._elementText.ToString();
										break;
									case "link":
										this._item.Link = new Uri(this._elementText.ToString());
										break;
									case "description":
										this._item.Description = this._elementText.ToString();
										break;
									case "author":
										this._item.Author = this._elementText.ToString();
										break;
									case "comments":
										this._item.Comments = this._elementText.ToString();
										break;
									case "pubdate":
										try
										{
											this._item.PubDate = DateTime.Parse(this._elementText.ToString());
										}
										catch (Exception e)
										{
											try
											{
												var tmp = this._elementText.ToString();
												tmp = tmp.Substring(0, tmp.Length - 5);
												tmp += "GMT";
												this._item.PubDate = DateTime.Parse(tmp);
											}
											catch
											{
												this.Exceptions.Add(e);
											}
										}
										break;
								}
								break;
							case "channel":
								switch (childElementName)
								{
									case "title":
										this._channel.Title = this._elementText.ToString();
										break;
									case "link":
										try
										{
											this._channel.Link = new Uri(this._elementText.ToString());
										}
										catch (Exception e)
										{
											this.Exceptions.Add(e);
										}
										break;
									case "description":
										this._channel.Description = this._elementText.ToString();
										break;
									case "language":
										this._channel.Language = this._elementText.ToString();
										break;
									case "copyright":
										this._channel.Copyright = this._elementText.ToString();
										break;
									case "managingeditor":
										this._channel.ManagingEditor = this._elementText.ToString();
										break;
									case "webmaster":
										this._channel.WebMaster = this._elementText.ToString();
										break;
									case "rating":
										this._channel.Rating = this._elementText.ToString();
										break;
									case "pubdate":
										try
										{
											this._channel.PubDate = DateTime.Parse(this._elementText.ToString());
										}
										catch (Exception e)
										{
											this.Exceptions.Add(e);
										}
										break;
									case "lastbuilddate":
										try
										{
											this._channel.LastBuildDate = DateTime.Parse(this._elementText.ToString());
										}
										catch (Exception e)
										{
											this.Exceptions.Add(e);
										}
										break;
									case "generator":
										this._channel.Generator = this._elementText.ToString();
										break;
									case "docs":
										this._channel.Docs = this._elementText.ToString();
										break;
									case "ttl":
										try
										{
											this._channel.TimeToLive = int.Parse(this._elementText.ToString());
										}
										catch (Exception e)
										{
											this.Exceptions.Add(e);
										}
										break;
								}
								break;
							case "image":
								switch (childElementName)
								{
									case "url":
										try
										{
											this._image.Url = new Uri(this._elementText.ToString());
										}
										catch (Exception e)
										{
											this.Exceptions.Add(e);
										}
										break;
									case "title":
										this._image.Title = this._elementText.ToString();
										break;
									case "link":
										try
										{
											this._image.Link = new Uri(this._elementText.ToString());
										}
										catch (Exception e)
										{
											this.Exceptions.Add(e);
										}
										break;
									case "description":
										this._image.Description = this._elementText.ToString();
										break;
									case "width":
										try
										{
											this._image.Width = Byte.Parse(this._elementText.ToString());
										}
										catch (Exception e)
										{
											this.Exceptions.Add(e);
										}
										break;
									case "height":
										try
										{
											this._image.Height = Byte.Parse(this._elementText.ToString());
										}
										catch (Exception e)
										{
											this.Exceptions.Add(e);
										}
										break;
								}
								break;
							case "textinput":
								switch (childElementName)
								{
									case "title":
										this._textInput.Title = this._elementText.ToString();
										break;
									case "description":
										this._textInput.Description = this._elementText.ToString();
										break;
									case "name":
										this._textInput.Name = this._elementText.ToString();
										break;
									case "link":
										try
										{
											this._textInput.Link = new Uri(this._elementText.ToString());
										}
										catch (Exception e)
										{
											this.Exceptions.Add(e);
										}
										break;
								}
								break;
							case "skipdays":
								if (childElementName == "day")
								{
									switch (this._elementText.ToString().ToLower())
									{
										case "monday":
											this._channel.SkipDays[0] = true;
											break;
										case "tuesday":
											this._channel.SkipDays[1] = true;
											break;
										case "wednesday":
											this._channel.SkipDays[2] = true;
											break;
										case "thursday":
											this._channel.SkipDays[3] = true;
											break;
										case "friday":
											this._channel.SkipDays[4] = true;
											break;
										case "saturday":
											this._channel.SkipDays[5] = true;
											break;
										case "sunday":
											this._channel.SkipDays[6] = true;
											break;
									}
								}
								break;
							case "skiphours":
								if (childElementName == "hour") this._channel.SkipHours[Byte.Parse(this._elementText.ToString().ToLower())] = true;
								break;
						}
						break;
					}
					case XmlNodeType.Text:
						this._elementText.Append(this._reader.Value);
						break;
					case XmlNodeType.CDATA:
						this._elementText.Append(this._reader.Value);
						break;
				}
			}
			while (readData);
			return rssElement;
		}

		/// <summary>Closes connection to file.</summary>
		/// <remarks>This method also releases any resources held while reading.</remarks>
		public void Close()
		{
			this._textInput = null;
			this._image = null;
			this._cloud = null;
			this._channel = null;
			this._source = null;
			this._enclosure = null;
			this._category = null;
			this._item = null;
			if (this._reader != null)
			{
				this._reader.Close();
				this._reader = null;
			}
			this._elementText = null;
			this._xmlNodeStack = null;
		}

		#region Constructors

		/// <summary>Initializes a new instance of the RssReader class with the specified URL or filename.</summary>
		/// <param name="url">The URL or filename for the file containing the RSS data.</param>
		/// <exception cref="ArgumentException">Occures when unable to retrieve file containing the RSS data.</exception>
		public RssReader(string url)
		{
			this.Exceptions = new ExceptionCollection();
			this.Version = RssVersion.Empty;
			try
			{
				this._reader = new XmlTextReader(url);
				this._InitReader();
			}
			catch (Exception e)
			{
				throw new ArgumentException("Unable to retrieve file containing the RSS data.", e);
			}
		}

		/// <summary>Creates an instance of the RssReader class using the specified TextReader.</summary>
		/// <param name="textReader">specified TextReader</param>
		/// <exception cref="ArgumentException">Occures when unable to retrieve file containing the RSS data.</exception>
		public RssReader(TextReader textReader)
		{
			this.Exceptions = new ExceptionCollection();
			this.Version = RssVersion.Empty;
			try
			{
				this._reader = new XmlTextReader(textReader);
				this._InitReader();
			}
			catch (Exception e)
			{
				throw new ArgumentException("Unable to retrieve file containing the RSS data.", e);
			}
		}

		/// <summary>Creates an instance of the RssReader class using the specified Stream.</summary>
		/// <exception cref="ArgumentException">Occures when unable to retrieve file containing the RSS data.</exception>
		/// <param name="stream">Stream to read from</param>
		public RssReader(Stream stream)
		{
			this.Exceptions = new ExceptionCollection();
			this.Version = RssVersion.Empty;
			try
			{
				this._reader = new XmlTextReader(stream);
				this._InitReader();
			}
			catch (Exception e)
			{
				throw new ArgumentException("Unable to retrieve file containing the RSS data.", e);
			}
		}

		#endregion
	}
}
